package org.jeecg.modules.gather.component;

import com.gargoylesoftware.htmlunit.ScriptException;
import com.gargoylesoftware.htmlunit.WebClient;
import com.gargoylesoftware.htmlunit.html.HtmlPage;
import com.gargoylesoftware.htmlunit.javascript.JavaScriptErrorListener;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.io.FilenameUtils;
import org.jeecg.modules.gather.seaweedfs.SeaweedFSProperties;
import org.jeecg.modules.gather.seaweedfs.SeaweedFSUtil;
import org.jetbrains.annotations.NotNull;
import org.jsoup.Jsoup;
import org.jsoup.nodes.Document;
import org.jsoup.nodes.Element;
import org.jsoup.select.Elements;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.stereotype.Component;
import us.codecraft.webmagic.Page;
import us.codecraft.webmagic.Site;
import us.codecraft.webmagic.Spider;
import us.codecraft.webmagic.processor.PageProcessor;

import java.io.IOException;
import java.net.MalformedURLException;
import java.net.URL;
import java.nio.charset.StandardCharsets;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.TimeUnit;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

@Component
@Slf4j
@EnableConfigurationProperties(SeaweedFSProperties.class)
public class DetailsPagePageProcessor implements PageProcessor {
    private SeaweedFSProperties seaweedFSProperties;
    private SeaweedFSUtil seaweedFSUtil;

    private Set<String> downloadUrls = new HashSet<>();
    private String url;

    @Autowired
    public void setSeaweedFSProperties(SeaweedFSProperties seaweedFSProperties) {
        this.seaweedFSProperties = seaweedFSProperties;
    }

    @Autowired
    public void setSeaweedFSUtil(SeaweedFSUtil seaweedFSUtil) {
        this.seaweedFSUtil = seaweedFSUtil;
    }

    @Override
    public Site getSite() {
        return Site.me()
                .setRetryTimes(1)
                .setSleepTime(1000)
                .setTimeOut(10000)
                .setCharset(StandardCharsets
                        .UTF_8.name());
    }

    @Override
    public void process(Page page) {
        String thisUrl = page.getUrl().toString();
        String fileExtension = FilenameUtils.getExtension(thisUrl).toLowerCase();
        if (isFileExists(page, fileExtension, thisUrl)) return;
        if (fileExtension.equals("html")) {
            String htmlString = downloaderHtml(thisUrl);
            page.setRawText(htmlString);
            saveHtml(page, fileExtension);
        } else {
            saveStaticResource(page, fileExtension);
        }
    }

    private boolean isFileExists(Page page, String fileExtension, String thisUrl) {
        String determineSubFolder = determineSubFolder(fileExtension);
        String localFileName = generateConsistentFileName(thisUrl, fileExtension);
        String staticResourceUrl = seaweedFSProperties.getUrl() + "/" + determineSubFolder + "/" + localFileName;
        String htmlFilePath = seaweedFSProperties.getUrl() + "/" + seaweedFSProperties.getWebPath() + "/" + localFileName;
        if (seaweedFSUtil.isFileExists(htmlFilePath)) {
            page.setSkip(true);
            return true;
        }
        if (seaweedFSUtil.isFileExists(staticResourceUrl)) {
            page.setSkip(true);
            return true;
        }
        return false;
    }

    private static String downloaderHtml(String thisUrl) {
        try (final WebClient webClient = new WebClient()) {
            webClient.getOptions().setJavaScriptEnabled(true);
            webClient.getOptions().setCssEnabled(false);
            webClient.getOptions().setThrowExceptionOnScriptError(false);
            webClient.getOptions().setThrowExceptionOnFailingStatusCode(false);
            webClient.getOptions().setTimeout((int) TimeUnit.SECONDS.toMillis(30));
            webClient.addRequestHeader("Accept", "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8");
            webClient.addRequestHeader("Accept-Language", "zh-CN,zh;q=0.8,en-US;q=0.5,en;q=0.3");
            webClient.addRequestHeader("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36");
            webClient.addRequestHeader("Referer", thisUrl);

            webClient.setJavaScriptErrorListener(new JavaScriptErrorListener() {
                @Override
                public void scriptException(HtmlPage page, ScriptException scriptException) {
                    log.warn("JavaScript执行错误: {}", scriptException.getMessage());
                }

                @Override
                public void timeoutError(HtmlPage page, long allowedTime, long executionTime) {
                    log.warn("JavaScript执行超时");
                }

                @Override
                public void warn(String s, String s1, int i, String s2, int i1) {
                }

                @Override
                public void malformedScriptURL(HtmlPage page, String url, MalformedURLException malformedURLException) {
                    log.warn("错误的脚本URL: {}", url);
                }

                @Override
                public void loadScriptError(HtmlPage page, URL scriptUrl, Exception exception) {
                    log.warn("脚本加载错误: {}", scriptUrl);
                }
            });
            HtmlPage pageHtml = webClient.getPage(thisUrl);
            webClient.waitForBackgroundJavaScript(5000);
            return pageHtml.asXml();

        } catch (IOException e) {
            throw new RuntimeException(e);
        }
    }

    private void saveStaticResource(Page page, String fileExtension) {
        String originalUrl = page.getUrl().toString();
        String subFolder = determineSubFolder(fileExtension);
        String localFileName = generateConsistentFileName(originalUrl, fileExtension);

        if ("css".equals(fileExtension) || "js".equals(fileExtension)) {
            byte[] contentBytes = page.getBytes();
            String content = new String(contentBytes, StandardCharsets.UTF_8);
            content = processResourceContent(originalUrl, content, fileExtension);
            saveToFile(content.getBytes(StandardCharsets.UTF_8), localFileName, subFolder);
        } else {
            saveToFile(page.getBytes(), localFileName, subFolder);
        }
    }

    private void saveHtml(Page page, String fileExtension) {
        this.url = page.getUrl().toString();
        Document document = Jsoup.parse(page.getHtml().get());
        processElements(page, document, "link[href]", "href", "css");
        processElements(page, document, "script[src]", "src", "js");
        processElements(page, document, "img[src]", "src", "images");
        processElements(page, document, "img[data-src]", "data-src", "images");
        processElements(page, document, "video[src]", "src", "videos");
        processElements(page, document, "source[src]", "src", "videos");
        cleanUpHyperlinks(document);
        processInlineStyles(document);
        String localFileName = generateConsistentFileName(url, fileExtension);
        String determineSubFolder = determineSubFolder(fileExtension);
        saveToFile(document.outerHtml().getBytes(StandardCharsets.UTF_8), localFileName, determineSubFolder);
    }

    private void processElements(Page page, Document document, String selector, String attr, String subFolder) {
        Elements elements = document.select(selector);
        for (Element element : elements) {
            String elementUrl = element.attr(attr);
            if (elementUrl == null || elementUrl.isEmpty() || elementUrl.startsWith("data:") || elementUrl.startsWith("#")) continue;
            String absoluteUrl = absoluteUrl(url, elementUrl);
            if (!isSameDomain(absoluteUrl)) continue;
            String fileExtension = FilenameUtils.getExtension(absoluteUrl).toLowerCase();
            String localFileName = generateConsistentFileName(absoluteUrl, fileExtension);
            String determineSubFolder = determineSubFolder(fileExtension);
            if (!downloadUrls.contains(absoluteUrl)) {
                page.addTargetRequest(absoluteUrl);
                downloadUrls.add(absoluteUrl);
            }
            String ossPath = getOssPath(determineSubFolder, localFileName);
            element.attr(attr, ossPath);
        }
    }

    private void cleanUpHyperlinks(Document document) {
        Elements hrefElements = document.select("a[href]");
        for (Element href : hrefElements) {
            href.attr("href", "#");
        }
        Elements targetElements = document.select("a[target]");
        for (Element target : targetElements) {
            target.attr("target", "");
        }
    }

    private String absoluteUrl(String url, String elementUrl) {
        try {
            URL base = new URL(url);
            URL absolute = new URL(base, elementUrl);
            return absolute.toString();
        } catch (Exception e) {
            return elementUrl;
        }
    }

    private boolean isSameDomain(String elementUrl) {
        try {
            URL target = new URL(elementUrl);
            URL base = new URL(url);
            return target.getHost().equalsIgnoreCase(base.getHost());
        } catch (Exception e) {
            return false;
        }
    }

    private String generateConsistentFileName(String url, String fileExtension) {
        String[] splitUrl = url.split("/");
        String fileName = splitUrl[splitUrl.length - 1];
        String replaceFileExtension = fileName.replace("." + fileExtension, "");
        return replaceFileExtension + "." + fileExtension;
    }

    private String determineSubFolder(String extension) {
        switch (extension) {
            case "css": return "css";
            case "js": return "js";
            case "jpg": case "jpeg": case "png": case "gif": case "svg": case "webp": return "images";
            case "woff": case "woff2": case "ttf": case "eot": return "fonts";
            case "mp4": case "webm": case "ogg": return "videos";
            case "html": return "html";
            default: return "resources";
        }
    }

    @NotNull
    private String getOssPath(String determineSubFolder, String localFileName) {
        return seaweedFSProperties.getUrl() + "/" + determineSubFolder + "/" + localFileName;
    }

    private void processInlineStyles(Document document) {
        Elements elementsWithStyle = document.select("[style]");
        Pattern urlPattern = Pattern.compile("url\\(['\"]?(.*?)['\"]?\\)");
        for (Element element : elementsWithStyle) {
            String style = element.attr("style");
            Matcher matcher = urlPattern.matcher(style);
            StringBuffer newStyle = new StringBuffer();

            while (matcher.find()) {
                String elementUrl = matcher.group(1);
                if (url.startsWith("data:")) {
                    matcher.appendReplacement(newStyle, "url(" + url + ")");
                    continue;
                }

                String absoluteUrl = absoluteUrl(url, elementUrl);
                if (isSameDomain(absoluteUrl)) {
                    Spider.create(this)
                            .addUrl(absoluteUrl)
                            .thread(6)
                            .run();

                    String fileExtension = FilenameUtils.getExtension(absoluteUrl).toLowerCase();
                    String localFileName = generateConsistentFileName(absoluteUrl, fileExtension);
                    String determineSubFolder = determineSubFolder(fileExtension);
                    String ossPath = getOssPath(determineSubFolder, localFileName);
                    matcher.appendReplacement(newStyle, "url(" + ossPath + ")");
                } else {
                    matcher.appendReplacement(newStyle, "url(" + url + ")");
                }
            }
            matcher.appendTail(newStyle);
            element.attr("style", newStyle.toString());
        }
    }

    private String processResourceContent(String resourceUrl, String content, String fileType) {
        Pattern pattern;
        if ("css".equals(fileType)) {
            pattern = Pattern.compile("url\\(\\s*([\"']?)(.*?)\\1\\s*\\)", Pattern.CASE_INSENSITIVE);
        } else if ("js".equals(fileType)) {
            pattern = Pattern.compile("(['\"])(.*?\\.(?:jpg|jpeg|png|gif|svg|webp))\\1", Pattern.CASE_INSENSITIVE);
        } else {
            return content;
        }

        Matcher matcher = pattern.matcher(content);
        StringBuffer sb = new StringBuffer();

        while (matcher.find()) {
            String originalUrl;
            String quote = "";
            if ("css".equals(fileType)) {
                quote = matcher.group(1);
                originalUrl = matcher.group(2);
            } else {
                quote = matcher.group(1);
                originalUrl = matcher.group(2);
            }

            if (originalUrl.startsWith("data:")) {
                matcher.appendReplacement(sb, matcher.group(0));
                continue;
            }

            String absoluteUrl = absoluteUrl(resourceUrl, originalUrl);
            if (!isSameDomain(absoluteUrl)) {
                matcher.appendReplacement(sb, matcher.group(0));
                continue;
            }

            if (!downloadUrls.contains(absoluteUrl)) {
                Spider.create(this)
                        .addUrl(absoluteUrl)
                        .thread(6)
                        .run();
                downloadUrls.add(absoluteUrl);
            }

            String fileExtension = FilenameUtils.getExtension(absoluteUrl).toLowerCase();
            String localFileName = generateConsistentFileName(absoluteUrl, fileExtension);
            String resourceSubFolder = determineSubFolder(fileExtension);
            String relativePath = seaweedFSProperties.getUrl() + "/" + resourceSubFolder + "/" + localFileName;

            String replacement;
            if ("css".equals(fileType)) {
                replacement = "url(" + quote + relativePath + quote + ")";
            } else {
                replacement = quote + relativePath + quote;
            }
            matcher.appendReplacement(sb, Matcher.quoteReplacement(replacement));
        }
        matcher.appendTail(sb);
        return sb.toString();
    }

    private void saveToFile(byte[] bytes, String fileName, String path) {
        Map<String, String> stringStringMap = seaweedFSUtil.uploadFileContent(path, fileName, bytes);
    }
}
